﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Windows;
using System.Xml;
using System.Reflection;
using Kokomo.Mirror.CliMetadata;
using System.Windows.Input;
using System.Xml.XPath;
using System.Text.RegularExpressions;
using System.Windows.Media;

namespace Kokomo.PeInspector.Reflection.Disassembly
{
	class XmlDocumentationBuilder
	{
		public XmlDocumentationBuilder(
			FlowDocument document,
			Assembly localAssembly,
			IAssemblyLoadContext loadContext,
			ICommand navigateToCommand
			)
		{
			if (document == null) throw new ArgumentNullException("document");
			if (localAssembly == null) throw new ArgumentNullException("localAssembly");
			if (loadContext == null) throw new ArgumentNullException("loadContext");
			if (navigateToCommand == null) throw new ArgumentNullException("navigateToCommand");

			this.DocumentBuilder = new FlowDocumentBuilder(document);
			this.LocalAssembly = localAssembly;
			this.LoadContext = loadContext;
			this.NavigateToCommand = navigateToCommand;

			document.Dispatcher.Invoke((Action)this.Initialize);
		}

		public Assembly LocalAssembly { get; private set; }
		public IAssemblyLoadContext LoadContext { get; private set; }
		public FlowDocumentBuilder DocumentBuilder { get; private set; }
		public ICommand NavigateToCommand { get; private set; }
		public Style HyperlinkStyle { get; private set; }

		private Dictionary<XmlDocumentationTextType, Style> _textStyleTable;
		private void Initialize()
		{
			Dictionary<XmlDocumentationTextType, Style> styleTable = new Dictionary<XmlDocumentationTextType, Style>();

			Style style;

			// Title
			style = new Style();
			style.Setters.Add(new Setter(Run.FontSizeProperty, 20.0));
			styleTable[XmlDocumentationTextType.Title] = style;
			// SectionHeading
			style = new Style();
			style.Setters.Add(new Setter(Run.FontSizeProperty, 16.0));
			style.Setters.Add(new Setter(Run.FontWeightProperty, FontWeights.Bold));
			styleTable[XmlDocumentationTextType.SectionHeading] = style;
			// InlineCode
			style = new Style();
			style.Setters.Add(new Setter(Run.FontWeightProperty, FontWeights.SemiBold));
			styleTable[XmlDocumentationTextType.InlineCode] = style;
			// CodeBlock
			style = new Style();
			style.Setters.Add(new Setter(Run.FontFamilyProperty, new FontFamily("Consolas")));
			styleTable[XmlDocumentationTextType.CodeBlock] = style;
			// Parameter
			style = new Style();
			style.Setters.Add(new Setter(Run.FontStyleProperty, FontStyles.Oblique));
			styleTable[XmlDocumentationTextType.Parameter] = style;
			// ParameterReference
			style = new Style();
			style.Setters.Add(new Setter(Run.FontWeightProperty, FontWeights.Bold));
			styleTable[XmlDocumentationTextType.ParameterReference] = style;

			// HyperlinkStyle
			style = new Style();
			style.Setters.Add(new Setter(Hyperlink.TextDecorationsProperty, null));
			this.HyperlinkStyle = style;

			this._textStyleTable = styleTable;
		}
		private Style GetStyle(XmlDocumentationTextType textType)
		{
			Style style;
			this._textStyleTable.TryGetValue(textType, out style);
			return style;
		}

		private static string NormalizeText(string text)
		{
			bool isSpace = false;
			StringBuilder builder = new StringBuilder(text.Length);
			foreach (var c in text)
			{
				if (char.IsWhiteSpace(c))
				{
					if (!isSpace) builder.Append(' ');
					isSpace = true;
				}
				else
				{
					isSpace = false;
					builder.Append(c);
				}
			}
			return builder.ToString();
		}
		public void WriteText(
			string text,
			XmlDocumentationTextType textType = XmlDocumentationTextType.Text
			)
		{
			Style style = this.GetStyle(textType);
			this.DocumentBuilder.WriteStyledText(NormalizeText(text), style);
		}
		public void WriteLineBreak()
		{
			this.DocumentBuilder.WriteLineBreak();
		}
		public void NewParagraph()
		{
			this.DocumentBuilder.NewParagraph();
		}
		public void WriteXml(XmlReader subtree)
		{
			bool first = true;
			while (subtree.Read())
			{
				switch (subtree.NodeType)
				{
					case XmlNodeType.Element:
						switch (subtree.LocalName)
						{
							case "c":
								subtree.MoveToContent();
								this.WriteText(subtree.Value.Trim(), XmlDocumentationTextType.InlineCode);
								subtree.MoveToElement();
								break;
							case "para":
								this.DocumentBuilder.NewParagraph();
								subtree.MoveToContent();
								break;
							case "code":
								subtree.MoveToContent();
								subtree.Read();
								this.DocumentBuilder.NewParagraph();
								this.WriteText(subtree.Value.Trim(), XmlDocumentationTextType.CodeBlock);
								subtree.MoveToElement();
								break;
							case "list":
								string listType = subtree.GetAttribute("type");
								// TODO: Finish list
								break;
							case "typeparamref":
							case "paramref":
								string paramName = subtree.GetAttribute("name");
								this.WriteText(paramName, XmlDocumentationTextType.ParameterReference);
								break;
							case "see":
								string cref = subtree.GetAttribute("cref");
								if (cref != null && cref.Length > 2)
									this.WriteMemberReference(cref);
								break;
							default:
								break;
						}
						break;
					case XmlNodeType.Entity:
						break;
					case XmlNodeType.EntityReference:
						break;
					case XmlNodeType.SignificantWhitespace:
					case XmlNodeType.Text:
						string text = subtree.Value;
						if (first && (text != null))
						{
							text = text.TrimStart();
							first = false;
						}

						this.WriteText(text);
						break;
					case XmlNodeType.Whitespace:
						break;
				}
				first = false;
			}

			// exception
			// include
			// param
			// permission
			// seealso
		}

		private Type ResolveType(string typeName)
		{
			// Try local assembly
			Type type = this.LocalAssembly.GetType(typeName, false);
			if (type != null) return type;
			// Try references
			foreach (var reference in this.LocalAssembly.GetReferencedAssemblies())
			{
				Assembly referenced = this.LoadContext.LoadAssembly(reference, this.LocalAssembly);
				if (reference != null)
				{
					type = referenced.GetType(typeName, false);
					if (type != null) return type;
				}
			}

			return null;
		}
		private MemberInfo ResolveMemberReference(string cref)
		{
			Match match = Regex.Match(cref, @"(?<prefix>\w):(?<member>(\w|\.)+)(\((,?(?<parameter>(\w|\.)+))+\))?");
			if (!match.Success) return null;

			string prefix = match.Groups["prefix"].Value;
			string memberName = match.Groups["member"].Value;
			bool isType = prefix == "T";

			string typeName;
			if (isType)
			{
				typeName = memberName;
			}
			else
			{
				int iDot = memberName.LastIndexOf('.');
				if (iDot > 0)
				{
					typeName = memberName.Substring(0, iDot);
					memberName = memberName.Substring(iDot + 1);
				}
				else
				{
					return null;
				}
			}

			Type type = this.ResolveType(typeName);
			if (type == null) return null;

			if (isType) return type;

			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

			MemberInfo member;
			bool isMethod = prefix == "M";
			if (isMethod)
			{
				Group paramGroup = match.Groups["parameter"];
				int parameterCount = paramGroup.Captures.Count;
				Type[] parameterTypes = new Type[parameterCount];

				for (int i = 0; i < parameterCount; i++)
				{
					string parameterTypeName = paramGroup.Captures[i].Value;
					Type parameterType = this.ResolveType(parameterTypeName);
					if (parameterType == null) return null;
					parameterTypes[i] = parameterType;
				}

				member = type.GetMethod(memberName, bindingFlags, null, parameterTypes, null);
			}
			else
			{
				member = type.GetMember(memberName, bindingFlags).FirstOrDefault();
			}

			return member;
		}
		public void WriteMemberReference(string cref, XmlDocumentationTextType textType = XmlDocumentationTextType.InlineCode)
		{
			MemberInfo member = this.ResolveMemberReference(cref);

			if (member == null)
			{
				this.WriteText(cref, XmlDocumentationTextType.InlineCode);
				return;
			}

			Style style = this.GetStyle(textType);
			this.DocumentBuilder.WriteHyperlink(
				member.Name,
				style,
				this.HyperlinkStyle,
				this.NavigateToCommand,
				member
				);
		}

		public void WriteSection(
			XPathNavigator navigator,
			string xpath,
			string sectionHeading = null
			)
		{
			if (navigator == null) throw new ArgumentNullException("navigator");
			if (xpath == null) throw new ArgumentNullException("xpath");

			XPathNavigator sectionNavigator = navigator.SelectSingleNode(xpath);
			if (sectionNavigator == null) return;

			this.NewParagraph();
			if (sectionHeading != null)
			{
				this.WriteText(sectionHeading, XmlDocumentationTextType.SectionHeading);
				this.NewParagraph();
			}

			var subtree = sectionNavigator.ReadSubtree();
			subtree.Read();

			this.WriteXml(subtree);
		}
	}
}
